Skip to content

feat(rig): provision .claude/worktrees ignore via global core.excludesfile (supersedes #23)#29

Merged
alex-mextner merged 2 commits into
mainfrom
rig-global-excludesfile
Jun 17, 2026
Merged

feat(rig): provision .claude/worktrees ignore via global core.excludesfile (supersedes #23)#29
alex-mextner merged 2 commits into
mainfrom
rig-global-excludesfile

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

What & why

Rebuilds #23's gitignore feature under the CTO-chosen GLOBAL design.

#23 wrote a managed block into each repo's committed .gitignore (per-repo, needs a rig apply per repo). The CTO rejected that and chose a global git excludesfile: rig manages ONE marker block in git's global core.excludesfile, so **/.claude/worktrees/ is ignored in every repo on the machinezero per-repo commits, no per-repo rig apply.

This reuses #23's tested marker-reconcile machine (create/update/ok/conflict/io_error, the offset-based splice, drift parity, the gitignore: schema), retargeted to the global file.

Design

Global config, wired like the git-hooks dispatcher — a git config --global setting plus a managed file, living in the GLOBAL rig layer. Default ON at plan level (an absent gitignore key still provisions), so the global block is not scaffolded into committed repo rig.yaml.

Target resolution (at apply time):

  • core.excludesfile already set (this machine: ~/.gitignore) → manage the block in that file, leave git config alone (respect the user's choice).
  • core.excludesfile unset → set it to the XDG default ~/.config/git/ignore and write the block there. So on a clean machine rig init does everything itself (set the config if absent + write the block).
  • gitignore.excludesfile: override → force a specific file.

Managed block — fenced by # >>> rig-managed (do not edit) >>> / # <<< … <<<, a fixed explanatory comment, then the entries (default ["**/.claude/worktrees/"], configurable). Only the bytes between the markers are ever touched; everything else (CRLF, trailing blanks, no-final-newline) is preserved verbatim.

Strict idempotency — a re-apply is a byte-identical no-op. If a prior non-idempotent tool appended the block more than once, rig collapses the managed region to one correct block, preserving any user line that sits between duplicated blocks (splice per marker-pair, not first-begin..last-end). .serena/ is deliberately not ignored (committed shared memory).

Driftrig status flags a missing/divergent/duplicated block, and an unset core.excludesfile rig would set, in the global section. rig apply reconciles.

Tests / smoke

  • uv run pytest tests/509 passed.
  • bash tests/smoke.shexit 0 (HOME-isolated leg asserts core.excludesfile set to the XDG default, the block written, and markers=2 byte-stable across re-apply; pytest leg green).
  • No test or smoke ever runs real git config --global or writes the real ~/.gitignore: the git-config read/write seams are stubbed suite-wide and HOME/XDG are isolated.
  • Zero-churn no-op confirmed against this machine's existing ~/.gitignore block: resolve_global_excludes(...)ok (no rewrite).
  • Ran multi-model review on the diff; addressed findings (interleaved-user-line preservation, XDG test isolation, removed the global block from the committed repo scaffold, doctor→status doc fix, CRLF create-path test, SYNC note on the duplicated path-expander).

Supersedes #23

#23 should be closed as superseded — leaving that to the CTO (not closing it here).

Notes

🤖 Generated with Claude Code

alex-mextner and others added 2 commits June 17, 2026 06:57
…sfile

Rebuild #23's gitignore feature under the CTO-chosen GLOBAL design: rig owns
ONE marker-delimited block in git's global core.excludesfile so the harness's
throwaway **/.claude/worktrees/ is ignored in EVERY repo on the machine, with
zero per-repo commits and no per-repo `rig apply`.

Wired like the git-hooks dispatcher (a `git config --global` setting + a
managed file), in the GLOBAL rig layer. Target resolution at apply time:
honor an existing core.excludesfile; when unset, set it to ~/.config/git/ignore
AND write the block there (so a clean machine is fully provisioned by `rig
init` alone). Reuses #23's marker reconcile machine (create/update/ok/conflict/
io_error), retargeted, plus:
  - strict idempotency: a re-apply is a byte-identical no-op;
  - dedup of the managed region: several duplicated blocks collapse to one,
    preserving user content BETWEEN blocks (splice per marker-pair, not
    first-begin..last-end);
  - drift in the GLOBAL section flags a missing/divergent/duplicated block and
    an unset core.excludesfile rig would set.

Default ON at plan level (an absent `gitignore` key still provisions), so the
GLOBAL block is NOT scaffolded into committed repo rig.yaml. The canonical
block text (markers + fixed comment + entry) is byte-identical to what a
provisioned machine already has, making re-apply zero-churn.

Review: ran multi-model `review` on this diff; addressed findings (interleaved
user-line preservation, XDG isolation in tests, scaffold cleanup, doc fixes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- tests/test_global_excludes.py: target resolution both ways (core.excludesfile
  set vs unset, with injected git-config seams so no test runs real `git config
  --global`), every reconcile state, STRICT byte-identical re-apply, the
  dedup-of-managed-region collapse (incl. preserving a user line between blocks),
  CRLF/trailing-blank/no-final-newline preservation, drift parity, disabled-but-
  installed leftover scan, and a byte-stable canonical-block regression pin.
- conftest: autouse fixtures isolate HOME + XDG_CONFIG_HOME and stub the
  git-config read/write seams suite-wide, so a full-plan e2e test can never
  touch the real ~/.gitignore or real global git config.
- smoke.sh: HOME-isolated leg exercises the real init/apply flow — asserts
  core.excludesfile is set to the XDG default, the block is written, and the
  block is byte-stable (markers=2) across a re-apply. Also makes the mcp leg
  conditional on the carrier and prefers `uv run pytest`.
- docs/config-schema.md: the `gitignore` (global core.excludesfile) section and
  validation note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alex-mextner alex-mextner force-pushed the rig-global-excludesfile branch from 4a1b1f5 to 6c54306 Compare June 17, 2026 05:06
@alex-mextner alex-mextner marked this pull request as ready for review June 17, 2026 05:12
@alex-mextner alex-mextner merged commit 5ea78b0 into main Jun 17, 2026
14 checks passed
@alex-mextner alex-mextner deleted the rig-global-excludesfile branch June 17, 2026 05:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c54306724

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread riglib/actions/runner.py
Comment on lines +2250 to +2251
if xdg and (path_str == "~/.config" or path_str.startswith("~/.config/")):
path_str = xdg + path_str[len("~/.config"):]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve configured ~/.config paths the same way Git does

When $XDG_CONFIG_HOME is set and core.excludesfile is unset (or already ~/.config/git/ignore), this rewrite makes rig write the managed block to $XDG_CONFIG_HOME/git/ignore, while _set_git_global stores ~/.config/git/ignore; Git then expands that configured pathname to $HOME/.config/git/ignore, not XDG (verified with git config --type=path; the git-config docs say core.excludesFile defaults to $XDG_CONFIG_HOME/git/ignore only when the option is unset: https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreexcludesFile). In that environment apply/status report success, but Git does not ignore .claude/worktrees/.

Useful? React with 👍 / 👎.

alex-mextner added a commit that referenced this pull request Jun 17, 2026
…ixture

#27's clean-sample leg enumerates every default-ON category and disables
it to assert zero false drift. tg_ctl (added by this PR) is default-ON, so
its provision_tg_ctl action made the clean sample report drift (exit 3).
Mirror how #29 added 'gitignore: {enabled: false}' and disable tg_ctl too.
alex-mextner added a commit that referenced this pull request Jun 17, 2026
* feat(rig): add tg_ctl config block + pure plist planning

Add the tg_ctl config block (validate + plan) and the pure, effect-free
TgCtlPlan that renders the ai.hyperide.tg-ctl.plist LaunchAgent XML
byte-exact to the working hand-created file (sort_keys=False preserves the
insertion order so a re-apply is a true no-op). Default-on, per-machine
(GLOBAL layer), macOS-only. Mirrors the tmux block's schema style.

boot:null and label:null resolve to their defaults (not bool(None)=False /
str(None)="None").

Reviewed via multi-model `review`; findings addressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(rig): provision + reconcile tg-ctl boot LaunchAgent

Runner: _do_provision_tg_ctl writes the byte-exact plist, backs up a
differing prior, ensures the log dir, tears down the stale predecessor
(com.ultra.codex-tg-bot: bootout + timestamped backup + remove), and
(re)loads via launchctl bootout/bootstrap in the gui/<uid> domain. A
re-apply against the already-correct loaded plist is a skipped no-op.
RIG_TG_CTL_DRY_RUN writes the plist but skips every live/destructive
mutation (launchctl AND the stale teardown) so tests/smoke never touch the
real launchd domain.

Drift: _check_tg_ctl flags missing / divergent / written-but-not-loaded, a
leftover plist when boot:false, and the stale predecessor (extra). CLI:
GLOBAL status line shows installed / drifted / disabled / unsupported
(off-darwin), resolved through the shared plan builder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(rig): tg-ctl unit suite + HOME-isolated smoke leg

test_tg_ctl.py mirrors test_tmux.py: config validation, byte-exact plist
render (incl. against the live machine plist when present, read-only),
create/idempotent/conflict/dry-run states, stale-predecessor teardown, drift
(missing/modified/extra/not-loaded), status states, and the boot:null /
label:null / dry-run-no-stale-removal / off-darwin regressions.

conftest neutralizes the default-on tg_ctl provisioner + drift check and
stubs the gui-domain launchctl seams suite-wide (dedicated tests restore the
real ones with their own HOME-isolated tmp dirs); no test ever touches the
real ~/Library/LaunchAgents or runs real launchctl. smoke.sh gains a
focused, HOME-isolated, RIG_TG_CTL_DRY_RUN tg-ctl leg and prefers
`uv run --extra test pytest`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(rig): document the tg_ctl config block

docs/config-schema.md: the tg_ctl section (keys, defaults, the byte-exact
no-op contract, gui-domain (re)load, stale-predecessor teardown, drift, the
RIG_TG_CTL_DRY_RUN seam, and the enabled:false vs boot:false distinction) +
the validation paragraph. AGENTS.md: refine the "never mutate a LIVE service"
rule — the stateless background daemons (models cron, tg_ctl) are the
documented (re)load exceptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(rig): disable tg_ctl in the error-system-v2 clean-sample smoke fixture

#27's clean-sample leg enumerates every default-ON category and disables
it to assert zero false drift. tg_ctl (added by this PR) is default-ON, so
its provision_tg_ctl action made the clean sample report drift (exit 3).
Mirror how #29 added 'gitignore: {enabled: false}' and disable tg_ctl too.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant